package cron

import (
	"fmt"
	"github.com/bits-and-blooms/bitset"
	"strconv"
	"strings"
	"time"
)

var NIL_LOCATION = &time.Location{}

// 来自于Java spring的 CronSequenceGenerator.java 的go翻译
type CronExpression struct {
	expression  string
	loc         *time.Location
	months      *bitset.BitSet
	daysOfMonth *bitset.BitSet
	daysOfWeek  *bitset.BitSet
	hours       *bitset.BitSet
	minutes     *bitset.BitSet
	seconds     *bitset.BitSet
}

// NewCronExpression 创建表达式
// @param  cronExpression 表达式
// @return 表达式指针
func NewCronExpression(cronExpression string) *CronExpression {
	return New(cronExpression, NIL_LOCATION)
}

// New 创建表达式
// @param  cronExpression 表达式
// @param  loc 本地时区信息
// @return 表达式指针
func New(cronExpression string, loc *time.Location) *CronExpression {

	cron := &CronExpression{
		expression:  cronExpression,
		loc:         loc,
		months:      bitset.New(12),
		daysOfMonth: bitset.New(31),
		daysOfWeek:  bitset.New(7),
		hours:       bitset.New(24),
		minutes:     bitset.New(60),
		seconds:     bitset.New(60),
	}
	fields := parse(cronExpression)
	doParse(fields, cron)

	return cron
}

//	Get the next {@link time.Time} in the sequence matching the Cron pattern and
//	after the value provided. The return value will have a whole number of
//
// seconds, and will be after the input value.
// @param date a seed value
// @return the next value matching the pattern
func (ce *CronExpression) Next(t time.Time) time.Time {
	/*
		The plan:

		1 Start with whole second (rounding up if necessary)

		2 If seconds match move on, otherwise find the next match:
		2.1 If next match is in the next minute then roll forwards

		3 If minute matches move on, otherwise find the next match
		3.1 If next match is in the next hour then roll forwards
		3.2 Reset the seconds and go to 2

		4 If hour matches move on, otherwise find the next match
		4.1 If next match is in the next day then roll forwards,
		4.2 Reset the minutes and seconds and go to 2
	*/

	//t.Second()
	//t.Minute()
	//t.Hour()
	//t.Weekday()
	//t.Day()
	//t.Month()
	//t.Year()

	//t.In()
	t.Location()

	t.Unix()

	t4 := time.Date(2019, 9, 30, 14, 28, 26, 23, ce.loc) // 返回时间格式Time
	fmt.Println(t4)

	return t4
}

func (ce *CronExpression) DoNext(t time.Time) time.Time {

	return time.Time{}
}

// 表达式转化为数组,去掉空格
// @param expression cron表达式字符串
func parse(expression string) []string {

	s := strings.TrimSpace(expression)

	fields := make([]string, 0, 6)
	start, end := 0, 0

	for i, len := 0, len(s); i < len; i += 1 {

		if ' ' == s[start] {
			// 定位符号开始位置
			start++
			continue
		}

		end = i
		if end+1 == len || ' ' == s[end+1] {
			fields = append(fields, s[start:end+1])
			start = i + 1
		}

	}

	//fmt.Println(fields)

	return fields

}

// 表达式转化为数组,去掉空格
// @param fields cron表达式各个字段
// @param ce cron表达式对象
func doParse(fields []string, ce *CronExpression) {
	setNumberHits(ce.seconds, fields[0], 0, 60)
	setNumberHits(ce.minutes, fields[1], 0, 60)
	setNumberHits(ce.hours, fields[2], 0, 24)
	setDaysOfMonth(ce.daysOfMonth, fields[3])
	setMonths(ce.months, fields[4])
	setDays(ce.daysOfWeek, replaceOrdinals(fields[5], "SUN,MON,TUE,WED,THU,FRI,SAT"), 8)

	if ce.daysOfWeek.Test(7) {
		// Sunday can be represented as 0 or 7
		ce.daysOfWeek.Set(0)
		ce.daysOfWeek.Clear(7)
	}
}

func setNumberHits(bits *bitset.BitSet, value string, min int, max int) {
	fields := strings.Split(value, ",")

	for _, field := range fields {
		if !strings.Contains(field, "/") {
			// Not an incrementer so it must be a range (possibly empty)
			range0 := getRange(field, min, max)

			for i := range0[0]; i < range0[1]+1; i++ {
				bits.Set(uint(i))
			}

		} else {
			split := strings.Split(field, "/")
			if len(split) > 2 {
				panic("Range has more than two fields: '" +
					field + "' in expression ")
			}

			range0 := getRange(field, min, max)

			if strings.Contains(split[0], "-") {
				range0[1] = max - 1
			}

			delta, _ := strconv.Atoi(split[1])
			if delta <= 0 {
				panic("Incrementer delta must be 1 or higher: '" + field)
			}

			for i := range0[0]; i <= range0[1]; i += delta {
				bits.Set(uint(i))
			}

		}
	}

}

func getRange(field string, min int, max int) []int {
	result := make([]int, 0, 2)
	if strings.Contains(field, "*") {
		result[0] = min
		result[1] = max
		return result
	}

	if !strings.Contains(field, "-") {
		result[0], _ = strconv.Atoi(field)
		result[1], _ = strconv.Atoi(field)
	} else {
		split := strings.Split(field, "-")
		if len(split) > 2 {
			panic("Range has more than two fields: '" +
				field + "' in expression ")
		}
		result[0], _ = strconv.Atoi(split[0])
		result[1], _ = strconv.Atoi(split[1])

	}
	if result[0] >= max || result[1] >= max {
		panic("Range exceeds maximum (" + strconv.Itoa(max) + "): '" +
			field + "' in expression  ")
	}
	if result[0] < min || result[1] < min {
		panic("Range less than minimum (" + strconv.Itoa(min) + "): '" +
			field + "' in expression  ")
	}
	if result[0] > result[1] {
		panic("Invalid inverted range: '" + field +
			"' in expression  ")
	}

	return result

}

func setDaysOfMonth(bits *bitset.BitSet, field string) {
	max := 31
	// Days of month start with 1 (in Cron and Calendar) so add one
	setDays(bits, field, max+1)
	// ... and remove it from the front
	bits.Clear(0)
}

func setMonths(bits *bitset.BitSet, value string) {
	max := 12
	value = replaceOrdinals(value, "FOO,JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC")
	months := bitset.New(13)
	// Months start with 1 in Cron and 0 in Calendar, so push the values first into a longer bit set
	setNumberHits(months, value, 1, max+1)
	// ... and then rotate it to the front of the months
	for i := 1; i <= max; i++ {
		if months.Test(uint(i)) {
			bits.Set(uint(i - 1))
		}
	}
}

func setDays(bits *bitset.BitSet, field string, max int) {
	if strings.Contains(field, "?") {
		field = "*"
	}

	setNumberHits(bits, field, 0, max)

}

/**
 * Replace the values in the comma-separated list (case insensitive)
 * with their index in the list.
 * @return a new String with the values from the list replaced
 */
func replaceOrdinals(value string, commaSeparatedList string) string {
	list := strings.Split(commaSeparatedList, ",")
	for i := 0; i < len(list); i++ {
		item := strings.ToLower(list[i])
		value = strings.ReplaceAll(strings.ToUpper(value), item, strconv.Itoa(i))
	}

	return value
}
